# frozen_string_literal: true

require('singleton')

class Board
  def equiv?(other)
    return Equivalence.instance.mapping(self) == Equivalence.instance.mapping(other)
  end
end

class Equivalence
  include Singleton

  attr_reader(:equiv_codes)

  def initialize
    reset
  end

  def reset
    Game.instance.initial_board.tap do |i|
      @equiv_codes = Game.instance.board_chars.collect do |c|
        [c, self.class.compute_equiv_code(i, c)]
      end.to_h

      @symmetries = %i[flip_h flip_v].select do |s|
        code_board(i) == code_board(i.send(s))
      end << :itself
    end
  end

  def code_board(board)
    return Board.new(
      board.b.collect do |row|
        row.collect {|c| @equiv_codes[c]}
      end
    )
  end

  def mapping(board)
    @symmetries.collect {|s| code_board(board.send(s))}.min_by(&:hash)
  end

  # Assume the pieces are all rectangular
  # TODO: use PieceMask#piece_geometry
  def self.compute_equiv_code(board, c)
    PieceMask.nu(board, c).b.collect do |row|
      row.reject {|e| Game.instance.vide_char?(e)}
    end.reject(&:empty?).tap do |mask|
      raise 'bad piece' if mask.collect(&:join).uniq.size > 1

      return [
        Array(mask.first).count,
        mask.count
      ].join
    end
  end
end

class BoardHistory
  include Singleton

  def initialize
    clear
  end

  def clear
    Equivalence.instance.reset
    @depths = {}
  end

  def saw_at_depth(board, depth)
    Equivalence.instance.mapping(board).tap do |em|
      @depths[em] = [@depths[em], depth].compact.min
    end
  end

  def equiv_seen_depth(board)
    return @depths[Equivalence.instance.mapping(board)]
  end

  def seen_equiv_at_or_deeper?(board, depth)
    return false unless depth

    equiv_seen_depth(board).tap do |d|
      return !d.nil? && d <= depth
    end
  end
end
